[社内ハンズオン資料] Angular+Firebaseで作るTODOアプリケーション
どうも!大阪オフィスの西村祐二です。
大阪オフィスではフロントエンド勉強会を隔週で行っており、そこでAngular+Firebaseで作るTODOアプリケーションのハンズオン行ったのでその資料を公開します。
勉強のためのサンプルアプリケーションとしてお役に立てれば幸いです。
ゴール
下記のようなTODOアプリケーションを実装します。
主な機能として下記になります。
- TODOの追加削除
- Drag And DropでTODOを移動させてデータを更新
- Googleアカウントでログイン・ログアウト
- TODO追加入力フォームのバリデーション
バックエンドはFirebaseを利用しています。
利用するツール
- Angular
- Angular Material
- Firebase
- Hosting
- Authentication
- Firestore
対象者
- Angularをはじめたい人
- Firebaseをはじめたい人
- Angularの開発の雰囲気を掴みたい人
環境
Angular CLI: 9.1.1 Node: 12.13.0 OS: darwin x64 Angular: 9.1.1 ... animations, cli, common, compiler, compiler-cli, core, forms ... language-service, platform-browser, platform-browser-dynamic ... router Ivy Workspace: Yes Package Version ----------------------------------------------------------- @angular-devkit/architect 0.900.7 @angular-devkit/build-angular 0.901.1 @angular-devkit/build-optimizer 0.901.1 @angular-devkit/build-webpack 0.901.1 @angular-devkit/core 9.1.1 @angular-devkit/schematics 9.1.1 @angular/cdk 9.2.0 @angular/fire 6.0.0-rc.2 @angular/material 9.2.0 @ngtools/webpack 9.1.1 @schematics/angular 9.1.1 @schematics/update 0.901.1 rxjs 6.5.5 typescript 3.8.3 webpack 4.42.0
- firebase:7.14.0
流れ
- Angularアプリケーション雛形ファイルの作成
-
Firebaseのプロジェクトセットアップ
-
AngularとFirebaseを連携設定
-
Firebase Hostingデプロイ
-
TODOアプリケーション実装
-
動作確認
-
Firebase Hostingデプロイ
-
終わり
アプリケーションの雛形作成
▼CLIを使ってアプリケーションの雛形ファイルなど作成しながら進めるため公式が提供するAngular CLIをインストールします。
インストールするとng
というコマンドが利用できるようになります。
$ npm install -g @angular/cli
▼CLIを使ってアプリケーションの雛形ファイルを生成します。
ルーティングを制御するためのファイルをはじめから作成するために、--routing
というオプションを設定しておきます。また、CSSファイルはSCSSを利用するために--style=scss
のオプションを指定しておきます。
$ ng new demo-app --routing --style=scss $ cd demo-app
▼ファイルの作成が完了したら、ローカルサーバを起動しアプリケーションを確認してみましょう。
起動したらブラウザからhttp://localhost:4200/
にアクセスしてみましょう。すると、Angularのアプリケーションを確認することができると思います。
$ ng serve
※--open
をつけるど自動的にブラウザを開いてhttp://localhost:4200/
にアクセスしてくれます。
設定ファイルの変更
はじめにやっておいたほうがよい設定を行っておきます。
ビルドに厳密な型チェックをするように設定を変更しておきます。
{ "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "strict": true, "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "module": "esnext", "moduleResolution": "node", "importHelpers": true, "target": "es2015", "lib": [ "es2018", "dom" ] }, "angularCompilerOptions": { "strictTemplates": true, "strictInjectionParameters": true } }
コードフォーマットのためにprettier
を導入します。
$ yarn add prettier --dev --exact
※--exact
を指定すると正確なバージョンを指定してパッケージをインストールすることができます。 デフォルトでは、同じメジャーバージョンの最新のリリースが使用されます。
prettier
の設定ファイルを作成します。ここではyamlでファイル作成していますが、各自好みのファイル形式で作成してください。Angular CLIでファイル作成した際、基本的にsingleQuote
が使用されるので、ここではsingleQuote
を許可する設定を記述しておきます。
singleQuote: true
commit時に自動的にprettier
でフォーマットするように設定を追加しておきます。
必要なライブラリを追加します。
$ npx mrm lint-staged
package.json
に自動的に設定が追加されます。対象となるファイル形式を下記のように修正しておきます。
... "lint-staged": { "*.{ts,scss,html}": "prettier --write" } ...
Firebaseと連携
Firebaseの準備
プロジェクトの作成
プロジェクト作成しておきます。 ここではプロジェクト名を「todo-demo-app-angular」として作成しています。
プロジェクト作成の手順は下記画像を参考にしてください。
Firebaseのコンソール画面から作成していきます。
データベース(Firestore)の準備
TODOの情報などを格納するためのデータベースを作成します。
今回はハンズオンなので、テストモードでデータベースを作成します。
作成する際にリージョンは日本のリージョンを選択しておくと良いです。
Authenticationの準備
アプリケーションにログイン機能を実装するため、認証のAuthenticationを設定します。
今回はGoogleアカウントを使ってログインできるようにしてみます。
アプリの作成
Firestoreのプロジェクト内にアプリを作成します。
歯車のアイコンをクリックし、「プロジェクトを設定」を開きます。
▼下の方になる「マイアプリ」のところのwebのボタンをクリックします。
▼登録するアプリの名前を設定します。ここでは「todo-demo-app」とします。
Firebase Hostingの設定にもチェックを入れておきます。
あとは次へをクリックしていき、アプリを登録します。
▼アプリの登録が完了したら、表示が変化します。構成のラジオボタンをクリックします。
すると、アプリケーション側で利用するFirebaseの設定情報が表示されるので、コピーしておきます。
AngularアプリケーションとFirebaseを連携させる
▼Firebase CLIをインストール
$ npm install -g firebase-tools
▼Firebase CLIからloginしておきます。
❯ firebase login i Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you. ? Allow Firebase to collect CLI usage and error reporting information? No Visit this URL on this device to log in: https://accounts.google.com/o/oauth2/auth?client_id=xxxx..... Waiting for authentication... ✔ Success! Logged in as XXXXXXXXXX@gmail.com
▼Angularで利用するFirebaseの公式Angularライブラリをインストールします。その際にプロジェクトを選択する際は、先程作成したtodo-demo-app-angular
を選択します。
$ yarn add firebase $ ng add @angular/fire@next Installing packages for tooling via yarn. Installed packages for tooling via yarn. UPDATE package.json (1707 bytes) ✔ Packages installed successfully. ✔ Preparing the list of your Firebase projects ? Please select a project: todo-demo-app-angular (todo-demo-app-angular) CREATE firebase.json (316 bytes) CREATE .firebaserc (157 bytes) UPDATE angular.json (3812 bytes)
▼先程コピーしたFirebaseの設定情報を参考にアプリケーションに追加します。
これで、アプリケーションからFirebaseに対して操作することができます。
設定情報はenvironment.ts
とenvironment.prod.ts
の両方に追記しておいてください。
export const environment = { production: false, firebase: { apiKey: '<your-key>', authDomain: '<your-project-authdomain>', databaseURL: '<your-database-URL>', projectId: '<your-project-id>', storageBucket: '<your-storage-bucket>', messagingSenderId: '<your-messaging-sender-id>', appId: '<your setting>', measurementId: '<your setting>' } };
▼AngularFireModuleを@NgModule
に設定していきます。
Angularでは大きくmodule単位で管理されており、利用したい機能のModuleをimportすることでアプリケーション側で利用できるようになります。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AngularFireModule } from '@angular/fire'; import { AngularFireAnalyticsModule } from '@angular/fire/analytics'; import { AngularFirestoreModule } from '@angular/fire/firestore'; import { environment } from 'src/environments/environment'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, AngularFireModule.initializeApp(environment.firebase), AngularFireAnalyticsModule, AngularFirestoreModule, ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
Firebase Hostingにデプロイしてみる
ng add @angular/fire@next
のインストールが完了すると自動的にFirebaseにHostingするための設定ファイルが追加・更新されます。
少し設定ファイルを覗いてみます。
▼Angularのアプリケーションの設定ファイルにdeployに関する設定が追加されています。
... "deploy": { "builder": "@angular/fire:deploy", "options": {} } ...
▼firebaseの設定ファイルが作成されています。
中にはfirebaseでHostingするための設定がアプリケーションの環境をもとに設定されています、
{ "hosting": [ { "target": "test-firebase", "public": "dist/test-firebase", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] } ] }
▼firebaseの設定ファイルが作成されています。
{ "targets": { "todo-demo-app-angular": { "hosting": { "test-firebase": [ "todo-demo-app-angular" ] } } } }
▼Firebase Hostingにデプロイしてみます。
動作確認も兼ねてFirebase Hostingにデプロイしてみましょう。
方法はとても簡単でng deploy
するだけです。設定などはインストール時に自動的に設定されています。
$ ng deploy Compiling @angular/animations : es2015 as esm2015 Compiling @angular/compiler/testing : es2015 as esm2015 Compiling @angular/core : es2015 as esm2015 Compiling @angular/common : es2015 as esm2015 Compiling @angular/animations/browser : es2015 as esm2015 Compiling @angular/core/testing : es2015 as esm2015 Compiling @angular/animations/browser/testing : es2015 as esm2015 Compiling @angular/platform-browser : es2015 as esm2015 Compiling @angular/common/http : es2015 as esm2015 Compiling @angular/platform-browser/testing : es2015 as esm2015 Compiling @angular/platform-browser/animations : es2015 as esm2015 Compiling @angular/common/testing : es2015 as esm2015 Compiling @angular/platform-browser-dynamic : es2015 as esm2015 Compiling @angular/router : es2015 as esm2015 Compiling @angular/forms : es2015 as esm2015 Compiling @angular/common/http/testing : es2015 as esm2015 Compiling @angular/platform-browser-dynamic/testing : es2015 as esm2015 Compiling @angular/router/testing : es2015 as esm2015 Generating ES5 bundles for differential loading... ES5 bundle generation complete. chunk {0} runtime-es2015.f8b979f66300b1e53384.js (runtime) 1.45 kB [entry] [rendered] chunk {0} runtime-es5.f8b979f66300b1e53384.js (runtime) 1.45 kB [entry] [rendered] chunk {2} polyfills-es2015.a2c1af2b1be41024173b.js (polyfills) 36.1 kB [initial] [rendered] chunk {3} polyfills-es5.85d83c3040e41a367f6b.js (polyfills-es5) 129 kB [initial] [rendered] chunk {1} main-es2015.e2412a31842dacf9e991.js (main) 216 kB [initial] [rendered] chunk {1} main-es5.e2412a31842dacf9e991.js (main) 258 kB [initial] [rendered] chunk {4} styles.09e2c710755c8867a460.css (styles) 0 bytes [initial] [rendered] Date: 2020-04-11T09:17:24.577Z - Hash: 6bf4e5c2889ba4665d88 - Time: 36148ms ? Your application is now available at https://todo-demo-app-angular.firebaseapp.com/
デプロイが完了したら表示されたURLにアクセスしてみましょう。
アプリケーション実装
Firebaseとの連携ができたので、次はAngularアプリケーションの実装を行っていきます。
Angular Materialを導入
UI部分の実装工数をへらすために、Angular公式が提供するUIフレームワークのAngular Materialをインストールします。
インストールの際に対話形式でいくつか質問されます。
- custom theme:基本となるカラーテーマを選択します。今回は
Indigo/Pink
としておきます。 - 他きかれることは基本的にYesで問題ないです。
$ ng add @angular/material Installing packages for tooling via npm. Installed packages for tooling via npm. ? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink ? Set up global Angular Material typography styles? Yes ? Set up browser animations for Angular Material? Yes UPDATE package.json (1799 bytes) ✔ Packages installed successfully. UPDATE src/app/app.module.ts (860 bytes) UPDATE angular.json (3976 bytes) UPDATE src/index.html (547 bytes) UPDATE src/styles.scss (181 bytes)
ng add
コマンドでインストールすることで関連するライブラリのインストールや設定周りなどもまとめて面倒みてくれます。
Materialモジュール作成
Angular Materialはいろんなところで利用するためモジュール化しておきます。
Angular Materialの見た目を使いたいときは、MatIconModule
のようなモジュールをimportする必要があります。
importすることで、対象のモジュールにぶら下がるComponentの中でAngular Materialの見た目がつかえるようになります。
機能ごとにモジュールを分割していくと都度Angular Materialをimportする必要がでてきて面倒なので、まとめてimportするためのmaterial.moduleを作っておきます。
$ ng g module material
使いたいAngular Materialのmoduleをimportしておき、exportsに列挙していきます。
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatToolbarModule } from '@angular/material/toolbar'; import { DragDropModule } from '@angular/cdk/drag-drop'; @NgModule({ declarations: [], imports: [CommonModule], exports: [ MatToolbarModule, MatIconModule, MatButtonModule, MatFormFieldModule, MatInputModule, DragDropModule, ], }) export class MaterialModule {}
▼作成したMaterialモジュールをAppModuleにimportしてAppModuleに紐づくcomponetでAngular Materialを利用できるようにしておきます。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { MaterialModule } from 'src/app/material/material.module'; import { AngularFireModule } from '@angular/fire'; import { AngularFireAnalyticsModule } from '@angular/fire/analytics'; import { AngularFirestoreModule } from '@angular/fire/firestore'; import { environment } from 'src/environments/environment'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, AngularFireModule.initializeApp(environment.firebase), AngularFireAnalyticsModule, AngularFirestoreModule, BrowserAnimationsModule, MaterialModule, ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
▼AppModule配下のcomponentでAngular Materialがつかえるようになったのでapp.component.html
でヘッダーをスタイリングしてみましょう。
app.component.html
を下記ソースに置き換えます。
<mat-toolbar> <button mat-button>TODO</button> <span class="spacer"></span> <button mat-raised-button>ログイン</button> </mat-toolbar>
▼ローカルサーバを起動させて確認してみましょう。
$ ng serve
http://localhost:4200
にアクセスして下記のようなスタイルがあたった画面がでてきたらOKです。
Googleアカウントを使ったログイン・ログアウト機能を実装
Authサービス作成
FirebaseのAuthenticationを使ったログイン、ログアウトの機能を実装していきます。
認証関連の機能をまとめたサービスを作成します。
外部とのやり取りが発生するところはサービスに切り出しておきます。
$ ng g service services/auth
g
はgenerate
を省略した形になります。
作成されたauth.service.ts
に下記のようにログイン、ログアウトのロジックを実装していきます。
import { Injectable } from '@angular/core'; import { AngularFireAuth } from '@angular/fire/auth'; import { auth } from 'firebase'; @Injectable({ providedIn: 'root', }) export class AuthService { user$ = this.afAuth.user; constructor(private afAuth: AngularFireAuth) {} login() { this.afAuth .signInWithPopup(new auth.GoogleAuthProvider()) .then((result) => { console.log(result); }); } logout() { this.afAuth.signOut(); } }
ログインにはGoogleアカウントを利用するので、GoogleAuthProvider
を設定しています。
AngularFireを使うと少ない記述でログイン周りの実装ができます。
また、公式によるライブラリの提供であるため安心感がありますね。
ヘッダー部分の実装
先程作ったAuthService
をDIして、ログイン・ログアウトの機能を実装します。
user$
の$
は慣習的にobservable
を意味します。
もし、ログイン済であれば、user$
にデータが流れてくるので、簡易的なログイン状態の判定に利用できます。
import { Component } from '@angular/core'; import { AuthService } from './services/auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent { user$ = this.auth.user$; constructor(private auth: AuthService) {} login() { this.auth.login(); } logout() { this.auth.logout(); } }
▼ヘッダー部分の実装を下記のように修正します。
ログイン後に取得できるアカウント情報からphotoURL
を使って画像を表示させます。
未ログイン際は#showLogin
のDOMが表示されるように制御しています。
<mat-toolbar> <button mat-button>TODO</button> <span class="spacer"></span> <ng-container *ngIf="user$ | async as user; else showLogin"> <button mat-raised-button (click)="logout()"> ログアウト <img src="{{ user.photoURL }}" alt="" width="30px" /> </button> </ng-container> <ng-template #showLogin> <button mat-raised-button (click)="login()">ログイン</button> </ng-template> </mat-toolbar>
▼見た目をcssで調整します。
.spacer { flex: 1; } img { border-radius: 50%; }
▼ローカルサーバーを起動して、動作確認してみましょう。
ログインボタンをクリックするとGoogleアカウントの選択画面が表示され、ログインを許可するとログインボタンが変わり下記のようなGoogleアカウントのアイコン付きボタンに変化するようになります。
TODO機能を実装
次にメインの機能であるTODO部分を作っていきます。
todoモジュール作成
TODO機能をもたせるためのmoduleを作っていきます。
AngularCLIでtodo用のmoduleの雛形を作成します。
$ ng g module todo --routing
--routing
オプションでルーティング用のファイルも同時に生成してくれます。
g
はgenerate
を省略した形になります。
TODOを追加するためのフォームなどを利用するので、フレームワークが提供するフォーム関連のモジュールをimportしておきます。また、Angular Materialを利用できるようにMaterialModule
もimportしておきます。
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TodoRoutingModule } from './todo-routing.module'; import { MaterialModule } from 'src/app/material/material.module'; @NgModule({ declarations: [], imports: [ CommonModule, TodoRoutingModule, MaterialModule, FormsModule, ReactiveFormsModule, ], }) export class TodoModule {}
todoコンポーネント作成
次に画面を構成するコンポーネントを作成します。
AngularCLIを使ってコンポーネントの雛形を作成します。
$ ng g component todo/todo -m todo/todo.module $ ng g component todo/todo-item -m todo/todo.module.ts
-m
をつけることで、どのmoduleにcomponentを紐付けるか指定することができます。
todo
コンポーネントは主に振る舞いなどの役割をもたせる想定です。
todo-item
はtodoの表示の役割をもたせる想定です。
ルーティングの設定
作成したtodo.module
をapp.module
に紐付けます。
今回はルーティングのパスによって読み込むmoduleを指定する方法で紐付けます。
今回作るアプリケーションではあまり関係ないですが、パスごとにmoduleをimportすることでLazyLoadされるので規模が大きくなっても初期ロード時に重くなることを避けることができます。
ここの設定ではrootのパスにアクセスするとTodoModule
が呼ばれる挙動になります。
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: '', loadChildren: () => import('src/app/todo/todo.module').then((m) => m.TodoModule), }, { path: '**', redirectTo: '', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {}
TODOモジュール側のルーティング設定を行うtodo-routing.module.ts
でも、パスと対応するコンポーネントを設定します。
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { TodoComponent } from './todo/todo.component'; const routes: Routes = [{ path: '', component: TodoComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TodoRoutingModule {}
TodoModule
に紐付けられたコンポーネントを表示されるように修正します。
簡易的にngIf
を使って未ログイン状態のときはTODOの画面が表示されないようにしています。
<mat-toolbar> <button mat-button>TODO</button> <span class="spacer"></span> <ng-container *ngIf="user$ | async as user; else showLogin"> <button mat-raised-button (click)="logout()"> ログアウト <img src="{{ user.photoURL }}" alt="" width="30px" /> </button> </ng-container> <ng-template #showLogin> <button mat-raised-button (click)="login()">ログイン</button> </ng-template> </mat-toolbar> <ng-container *ngIf="user$ | async as user"> <router-outlet></router-outlet> </ng-container>
router-outlet
タグを使用することでルーティングに沿ったコンポーネントを紐付けてくれます。今回はtodo.component.html
が紐付けられています。
ローカルサーバを起動して確認してみましょう。
ログインするとtodo.component.html
の内容「todo works!」が表示されていることがわかります。
todoの型定義ファイル作成
todoはどんなデータを持っているか定義しておきます。
▼AngularCLIを使って雛形ファイルを作成します。
$ ng g interface interfaces/todo
今回は下記のようにしています。
export interface Todo { uid: string; name: string; list: 'todo' | 'doing' | 'done'; id: string; }
todoサービス作成
TODOデータはFirebaseのFirestoreに格納するので、Firestoreとやり取りするtodoサービスを作成します。
$ ng g service services/todo
TODO作成、更新、取得、削除するロジックを下記のように実装します。
import { Injectable } from '@angular/core'; import { AngularFirestore } from '@angular/fire/firestore'; import { Todo } from 'src/app/interfaces/todo'; @Injectable({ providedIn: 'root', }) export class TodoService { constructor(private afs: AngularFirestore) {} createTodo(name: string, uid: string) { const id = this.afs.createId(); const params: Todo = { name, uid, id, list: 'todo', }; this.afs.collection<Todo>('todos').doc(id).set(params); } putTodo(params: Todo) { this.afs.collection<Todo>('todos').doc(params.id).set(params); } getTodos() { return this.afs.collection<Todo>('todos').valueChanges(); } deleteTodo(id: string) { this.afs.collection<Todo>('todos').doc(id).delete(); } }
実装のハマりどころはデータを登録するときに、ドキュメントIDをデータとして持たせて登録するところです。
理由としてはTODOデータを取得する時にvalueChanges()メソッドを使用していると、ドキュメントIDが取得できないので、更新・削除するときに困ってしまうからです。
今回は用意されているcreateId()
を使ってID生成し、そのIDをドキュメントIDと格納するデータの中にもたせています。
TODOを追加するフォームを実装
Angularはフォーム周りも公式で機能が提供されているので、これを利用して、TODOを追加するフォームを実装していきます。
フォームのバリデーションもある程度用意されているので実装はとても簡単です。
import { Component, OnInit } from '@angular/core'; import { tap } from 'rxjs/operators'; import { TodoService } from 'src/app/services/todo.service'; import { AuthService } from 'src/app/services/auth.service'; import { FormBuilder, Validators } from '@angular/forms'; @Component({ selector: 'app-todo', templateUrl: './todo.component.html', styleUrls: ['./todo.component.scss'], }) export class TodoComponent implements OnInit { todos$ = this.todo.getTodos().pipe(tap((data) => console.log(data))); user$ = this.auth.user$; lists = ['todo', 'doing', 'done']; form = this.fb.group({ name: ['', [Validators.required, Validators.maxLength(10)]], }); constructor( private todo: TodoService, private auth: AuthService, private fb: FormBuilder ) {} ngOnInit(): void {} createTodo(uid: string) { const name = this.form.controls.name.value; this.todo.createTodo(name, uid); this.form.reset(); this.form.controls.name.setErrors({ required: null }); } }
13行目: TODOの登録が完了するとtodo$
からデータがながれてきます。
27行目: this.form.controls.name.value
はフォームのnameプロパティに入っているデータを取ってくる処理です。
29行目: TODOの登録ボタンがクリックされたら、フォームの中身リフレッシュされます。
30行目: フォームの中身がリフレッシュされるとrequiredのバリデーションのエラーメッセージが表示されてしまうので、最初は出ないようしています。
▼表示部分のテンプレート実装を行います。
[formGroup]="form"
とすることで、コンポーネント側とテンプレート側をバインディングすることができます。
<ng-container *ngIf="user$ | async as user"> <form class="todoForm" [formGroup]="form" (ngSubmit)="createTodo(user!.uid)"> <mat-form-field hintLabel="最大文字数"> <mat-label>TODO追加</mat-label> <input matInput type="text" formControlName="name" autocomplete="off" #input /> <!-- バリデーションエラー --> <mat-error *ngIf="form.controls.name.hasError('maxlength')"> 文字数オーバー </mat-error> <mat-error *ngIf="form.controls.name.hasError('required')"> 入力してください </mat-error> <mat-hint align="end"> {{ input.value?.length || 0 }}/10 </mat-hint> </mat-form-field> <button mat-icon-button [disabled]="!form.valid"> <mat-icon>send</mat-icon> </button> </form> </ng-container> <ng-container *ngFor="let todo of todos$ | async"> <div> {{ todo.name }} </div> </ng-container>
21行目:フォームがエラーのときボタンをクリックできないようにする判定処理は!form.valid
がTrueのときフォームにdisabled
の属性が有効となる処理を記述して制御しています。
フォーム周りはいろいろな機能があり詳しく知りたい方は下記を参考ください。
https://angular.io/guide/reactive-forms#grouping-form-controls
https://material.angular.io/components/form-field/overview
https://material.angular.io/components/input/overview
▼実装きたらローカルサーバーを起動して、動作確認してみましょう。
下記のようにTODOデータの追加、表示ができるようになります。
また、Firestoreにもデータが格納されているか確認しておきましょう。
TODOデータをDrag And Dropできるように実装
Angular Materialは見た目の部分だけではなく、UI以外の、ふるまいの機能だけ利用することもできます。
今回はDrag And Dropの機能を使いTODOの移動などできるようにし、UIはCSSでTrello風な画面にしてみます。
▼はじめに、TODOデータを表示する役割をTodoItemComponent
に移譲させます。
親コンポーネントがTodoComponent
,子コンポーネントがTodoItemComponent
という関係になります。
親コンポーネントから子コンポーネントにデータを渡せるように実装していきます。
@Input
が子コンポーネントが親コンポーネントからデータを受け取る設定です。
@Output
が子コンポーネントから親コンポーネントにデータを渡して関数を実行するための設定です。
今回は子コンポーネントの削除ボタンがクリックされると親コンポーネントにあるdeleteTodo
が実行されるようにします。
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Todo } from 'src/app/interfaces/todo'; @Component({ selector: 'app-todo-item', templateUrl: './todo-item.component.html', styleUrls: ['./todo-item.component.scss'], }) export class TodoItemComponent implements OnInit { @Input() todo!: Todo; @Output() deleteTodo = new EventEmitter<string>(); constructor() {} ngOnInit(): void {} remove(id: string) { this.deleteTodo.emit(id); } }
<ng-container *ngIf="todo"> <div> {{ todo.name }} <button (click)="remove(todo.id)" mat-icon-button> <mat-icon>clear</mat-icon> </button> </div> </ng-container>
▼次はtodo.component.ts
にDropしたときにデータを更新する処理、TODOデータを削除する処理、listによって表示を分ける処理など追加します。
import { Component, OnInit } from '@angular/core'; import { tap } from 'rxjs/operators'; import { TodoService } from 'src/app/services/todo.service'; import { AuthService } from 'src/app/services/auth.service'; import { FormBuilder, Validators } from '@angular/forms'; import { Todo } from 'src/app/interfaces/todo'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'app-todo', templateUrl: './todo.component.html', styleUrls: ['./todo.component.scss'], }) export class TodoComponent implements OnInit { todos$ = this.todo.getTodos().pipe(tap((data) => console.log(data))); user$ = this.auth.user$; lists = ['todo', 'doing', 'done']; form = this.fb.group({ name: ['', [Validators.required, Validators.maxLength(10)]], }); constructor( private todo: TodoService, private auth: AuthService, private fb: FormBuilder ) {} ngOnInit(): void {} createTodo(uid: string) { const name = this.form.controls.name.value; this.todo.createTodo(name, uid); this.form.reset(); this.form.controls.name.setErrors({ required: null }); } deleteTodo(id: string) { this.todo.deleteTodo(id); } filterTodo(filterList: string, todos: Todo[]): Todo[] { if (!todos) { return []; } return todos.filter((todo) => todo.list === filterList); } drop(event: CdkDragDrop<Todo[]>) { console.log(event); if (event.previousContainer !== event.container) { const list = event.container.id; const todo = event.item.data; todo.list = list; this.todo.putTodo(todo); } } }
▼テンプレート側todo.component.html
の実装をしていきます。
Drag And Dropは要素にcdkDrag
を追加するだけて、要素を自由に動かせるようになります。
<ng-container *ngIf="user$ | async as user"> <form class="todoForm" [formGroup]="form" (ngSubmit)="createTodo(user!.uid)"> <mat-form-field hintLabel="最大文字数"> <mat-label>TODO追加</mat-label> <input matInput type="text" formControlName="name" autocomplete="off" #input /> <!-- バリデーションエラー --> <mat-error *ngIf="form.controls.name.hasError('maxlength')"> 文字数オーバー </mat-error> <mat-error *ngIf="form.controls.name.hasError('required')"> 入力してください </mat-error> <mat-hint align="end"> {{ input.value?.length || 0 }}/10 </mat-hint> </mat-form-field> <button mat-icon-button [disabled]="!form.valid"> <mat-icon>send</mat-icon> </button> </form> </ng-container> <div class="todolist" cdkDropListGroup> <ng-container *ngFor="let list of lists"> <div class="todolist__container mat-elevation-z2"> <div class="todolist__label">{{ list }}</div> <div id="{{ list }}" class="todolist__box" cdkDropList [cdkDropListData]="filterTodo(list, (todos$ | async)!)" (cdkDropListDropped)="drop($event)" > <app-todo-item class="todolist__item mat-elevation-z1" *ngFor="let todo of filterTodo(list, (todos$ | async)!)" cdkDrag [cdkDragData]="todo" [todo]="todo" (deleteTodo)="deleteTodo($event)" ></app-todo-item> </div> </div> </ng-container> </div>
▼Trello風な見た目にするためにCSSを記述します
.todolist { display: flex; margin-left: 10px; &__container { background-color: #ebecf0; border-radius: 3px; width: 300px; max-width: 100%; margin: 10px; display: inline-block; vertical-align: top; } &__label { width: 100%; font-size: 20px; font-weight: bold; margin: 10px 0px 0px 10px; } &__box { min-height: 60px; overflow: hidden; display: block; } &__item { padding: 10px; margin: 10px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; box-sizing: border-box; cursor: move; background: white; border-radius: 3px; } } .todoForm { margin: 20px; } .cdk-drag-preview { box-sizing: border-box; border-radius: 4px; box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); } .cdk-drag-placeholder { opacity: 0; } .cdk-drag-animating { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } .todolist__item:last-child { border: none; } .todolist__box.cdk-drop-list-dragging .todolist__item:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
これで実装は終わりです。
▼ローカルサーバを起動して動作確認してみましょう。
下記のような挙動をするアプリケーションができていれば完成です。
最後にTODOアプリケーションをデプロイ
アプリケーションが完成したらFirebase Hostingにデプロイしてみましょう。
方法はng deploy
するだけです。
$ ng deploy
デプロイが完了したら、表示されるURLにアクセスしTODOアプリケーションが動くことを確認しましょう。
これでハンズオンは終了です。お疲れ様でした。もし、余裕があれば下記課題にチャレンジしてみてください。
課題
登録したTODOの名前を変更する機能の実装
今の状態だと登録したTODOの名前の修正ができないので、名前修正機能を実装してみましょう。
登録したTODOの並び替え機能の実装
今の状態だと、Drag And Dropしたあと、サーバー側でソートされた状態で表示されてしまいます。
DropしたところにTODOが来るように修正してみましょう。
おわりに
Angular + Firebaseは開発元が近いこともあって、連携も簡単で開発体験はとてもよかったです。準備の段階ではFirebaseはほとんど触れたことがなかったのですがスムーズに実装・準備することができました。
ハンズオンの内容としては雑なところがありますが、Angular + Firebaseを使った開発の雰囲気が伝われば幸いです。